<?php

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Appender for writing to MongoDB.
 *
 * This class was originally contributed by Vladimir Gorej.
 *
 * ## Configurable parameters: ##
 *
 * - **host** - Server on which mongodb instance is located.
 * - **port** - Port on which the instance is bound.
 * - **databaseName** - Name of the database to which to log.
 * - **collectionName** - Name of the target collection within the given
 * database.
 * - **username** - Username used to connect to the database.
 * - **password** - Password used to connect to the database.
 * - **timeout** - For how long the driver should try to connect to the database
 * (in milliseconds).
 *
 * @version $Revision: 1346363 $
 * @package log4php
 * @subpackage appenders
 * @since 2.1
 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version
 *          2.0
 * @link http://logging.apache.org/log4php/docs/appenders/mongodb.html Appender
 *       documentation
 * @link http://github.com/log4mongo/log4mongo-php Vladimir Gorej's original
 *       submission.
 * @link http://www.mongodb.org/ MongoDB website.
 */
class LoggerAppenderMongoDB extends LoggerAppender
{
    
    // ******************************************
    // ** Constants **
    // ******************************************
    
    /**
     * Default prefix for the {@link $host}.
     */
    const DEFAULT_MONGO_URL_PREFIX = 'mongodb://';

    /**
     * Default value for {@link $host}, without a prefix.
     */
    const DEFAULT_MONGO_HOST = 'localhost';

    /**
     * Default value for {@link $port}
     */
    const DEFAULT_MONGO_PORT = 27017;

    /**
     * Default value for {@link $databaseName}
     */
    const DEFAULT_DB_NAME = 'log4php_mongodb';

    /**
     * Default value for {@link $collectionName}
     */
    const DEFAULT_COLLECTION_NAME = 'logs';

    /**
     * Default value for {@link $timeout}
     */
    const DEFAULT_TIMEOUT_VALUE = 3000;
    
    // ******************************************
    // ** Configurable parameters **
    // ******************************************
    
    /**
     * Server on which mongodb instance is located.
     */
    protected $host;

    /**
     * Port on which the instance is bound.
     */
    protected $port;

    /**
     * Name of the database to which to log.
     */
    protected $databaseName;

    /**
     * Name of the collection within the given database.
     */
    protected $collectionName;

    /**
     * Username used to connect to the database.
     */
    protected $userName;

    /**
     * Password used to connect to the database.
     */
    protected $password;

    /**
     * Timeout value used when connecting to the database (in milliseconds).
     */
    protected $timeout;
    
    // ******************************************
    // ** Member variables **
    // ******************************************
    
    /**
     * Connection to the MongoDB instance.
     *
     * @var Mongo
     */
    protected $connection;

    /**
     * The collection to which log is written.
     *
     * @var MongoCollection
     */
    protected $collection;

    public function __construct($name = '')
    {
        parent::__construct($name);
        $this->host = self::DEFAULT_MONGO_URL_PREFIX . self::DEFAULT_MONGO_HOST;
        $this->port = self::DEFAULT_MONGO_PORT;
        $this->databaseName = self::DEFAULT_DB_NAME;
        $this->collectionName = self::DEFAULT_COLLECTION_NAME;
        $this->timeout = self::DEFAULT_TIMEOUT_VALUE;
        $this->requiresLayout = false;
    }

    /**
     * Setup db connection.
     * Based on defined options, this method connects to the database and
     * creates a {@link $collection}.
     */
    public function activateOptions()
    {
        try {
            $this->connection = new Mongo(sprintf('%s:%d', $this->host, $this->port), array(
                'timeout' => $this->timeout
            ));
            $db = $this->connection->selectDB($this->databaseName);
            if ($this->userName !== null && $this->password !== null) {
                $authResult = $db->authenticate($this->userName, $this->password);
                if ($authResult['ok'] == floatval(0)) {
                    throw new Exception($authResult['errmsg'], $authResult['ok']);
                }
            }
            $this->collection = $db->selectCollection($this->collectionName);
        } catch (MongoConnectionException $ex) {
            $this->closed = true;
            $this->warn(sprintf('Failed to connect to mongo deamon: %s', $ex->getMessage()));
        } catch (InvalidArgumentException $ex) {
            $this->closed = true;
            $this->warn(sprintf('Error while selecting mongo database: %s', $ex->getMessage()));
        } catch (Exception $ex) {
            $this->closed = true;
            $this->warn('Invalid credentials for mongo database authentication');
        }
    }

    /**
     * Appends a new event to the mongo database.
     *
     * @param LoggerLoggingEvent $event            
     */
    public function append(LoggerLoggingEvent $event)
    {
        try {
            if ($this->collection != null) {
                $this->collection->insert($this->format($event));
            }
        } catch (MongoCursorException $ex) {
            $this->warn(sprintf('Error while writing to mongo collection: %s', $ex->getMessage()));
        }
    }

    /**
     * Converts the logging event into an array which can be logged to mongodb.
     *
     * @param LoggerLoggingEvent $event            
     * @return array The array representation of the logging event.
     */
    protected function format(LoggerLoggingEvent $event)
    {
        $timestampSec = (int) $event->getTimestamp();
        $timestampUsec = (int) (($event->getTimestamp() - $timestampSec) * 1000000);
        
        $document = array(
            'timestamp' => new MongoDate($timestampSec, $timestampUsec),
            'level' => $event->getLevel()->toString(),
            'thread' => (int) $event->getThreadName(),
            'message' => $event->getMessage(),
            'loggerName' => $event->getLoggerName()
        );
        
        $locationInfo = $event->getLocationInformation();
        if ($locationInfo != null) {
            $document['fileName'] = $locationInfo->getFileName();
            $document['method'] = $locationInfo->getMethodName();
            $document['lineNumber'] = ($locationInfo->getLineNumber() == 'NA') ? 'NA' : (int) $locationInfo->getLineNumber();
            $document['className'] = $locationInfo->getClassName();
        }
        
        $throwableInfo = $event->getThrowableInformation();
        if ($throwableInfo != null) {
            $document['exception'] = $this->formatThrowable($throwableInfo->getThrowable());
        }
        
        return $document;
    }

    /**
     * Converts an Exception into an array which can be logged to mongodb.
     *
     * Supports innner exceptions (PHP >= 5.3)
     *
     * @param Exception $ex            
     * @return array
     */
    protected function formatThrowable(Exception $ex)
    {
        $array = array(
            'message' => $ex->getMessage(),
            'code' => $ex->getCode(),
            'stackTrace' => $ex->getTraceAsString()
        );
        
        if (method_exists($ex, 'getPrevious') && $ex->getPrevious() !== null) {
            $array['innerException'] = $this->formatThrowable($ex->getPrevious());
        }
        
        return $array;
    }

    /**
     * Closes the connection to the logging database
     */
    public function close()
    {
        if ($this->closed != true) {
            $this->collection = null;
            if ($this->connection !== null) {
                $this->connection->close();
                $this->connection = null;
            }
            $this->closed = true;
        }
    }

    /**
     * Sets the value of {@link $host} parameter.
     *
     * @param string $host            
     */
    public function setHost($host)
    {
        if (! preg_match('/^mongodb\:\/\//', $host)) {
            $host = self::DEFAULT_MONGO_URL_PREFIX . $host;
        }
        $this->host = $host;
    }

    /**
     * Returns the value of {@link $host} parameter.
     *
     * @return string
     */
    public function getHost()
    {
        return $this->host;
    }

    /**
     * Sets the value of {@link $port} parameter.
     *
     * @param int $port            
     */
    public function setPort($port)
    {
        $this->setPositiveInteger('port', $port);
    }

    /**
     * Returns the value of {@link $port} parameter.
     *
     * @return int
     */
    public function getPort()
    {
        return $this->port;
    }

    /**
     * Sets the value of {@link $databaseName} parameter.
     *
     * @param string $databaseName            
     */
    public function setDatabaseName($databaseName)
    {
        $this->setString('databaseName', $databaseName);
    }

    /**
     * Returns the value of {@link $databaseName} parameter.
     *
     * @return string
     */
    public function getDatabaseName()
    {
        return $this->databaseName;
    }

    /**
     * Sets the value of {@link $collectionName} parameter.
     *
     * @param string $collectionName            
     */
    public function setCollectionName($collectionName)
    {
        $this->setString('collectionName', $collectionName);
    }

    /**
     * Returns the value of {@link $collectionName} parameter.
     *
     * @return string
     */
    public function getCollectionName()
    {
        return $this->collectionName;
    }

    /**
     * Sets the value of {@link $userName} parameter.
     *
     * @param string $userName            
     */
    public function setUserName($userName)
    {
        $this->setString('userName', $userName, true);
    }

    /**
     * Returns the value of {@link $userName} parameter.
     *
     * @return string
     */
    public function getUserName()
    {
        return $this->userName;
    }

    /**
     * Sets the value of {@link $password} parameter.
     *
     * @param string $password            
     */
    public function setPassword($password)
    {
        $this->setString('password', $password, true);
    }

    /**
     * Returns the value of {@link $password} parameter.
     *
     * @return string
     */
    public function getPassword()
    {
        return $this->password;
    }

    /**
     * Sets the value of {@link $timeout} parameter.
     *
     * @param int $timeout            
     */
    public function setTimeout($timeout)
    {
        $this->setPositiveInteger('timeout', $timeout);
    }

    /**
     * Returns the value of {@link $timeout} parameter.
     *
     * @return int
     */
    public function getTimeout()
    {
        return $this->timeout;
    }

    /**
     * Returns the mongodb connection.
     *
     * @return Mongo
     */
    public function getConnection()
    {
        return $this->connection;
    }

    /**
     * Returns the active mongodb collection.
     *
     * @return MongoCollection
     */
    public function getCollection()
    {
        return $this->collection;
    }
}
